import * as argon2 from "argon2";
import exp from "constants";
import _ from 'lodash'

interface PasswordEncoder {
    encode: (password: string) => Promise<string>;
    verify: (encoded: string, password: string) => Promise<boolean>;
}

const ARGON2_PASSWORD_ENCODER: PasswordEncoder = {
    encode: async (password: string) => {
        return await argon2.hash(password);
    },
    verify: async (encoded: string, password: string) => {
        return await argon2.verify(encoded, password);
    },
};

class DelegatePasswordEncoder implements PasswordEncoder {
    private readonly encoders: Record<string, PasswordEncoder>;
    private readonly defaultEncoderId: string;

    private static readonly ENCODER_ID_PATTERN = /^[a-zA-Z0-9]+$/
    private static readonly HASH_PATTERN = /^\{[a-zA-Z0-9]+\}/

    constructor(encoders: Record<string, PasswordEncoder>, defaultEncoderId: string) {
        if (!(defaultEncoderId in encoders)) {
            throw new Error('No corresponding default encoder.')
        }

        this.encoders = {}
        for(const id in encoders) {
            if (_.isEmpty(id.match(DelegatePasswordEncoder.ENCODER_ID_PATTERN))) {
                throw new Error(`Encoder id "${id}" contains invalid characters.`)
            }
            this.encoders[`{${id}}`] = encoders[id]
        }

        this.defaultEncoderId = `{${defaultEncoderId}}`;
    }

    encode = async (password: string) => {
        const encoder = this.encoders[this.defaultEncoderId]
        const hash = await encoder.encode(password);
        return `${this.defaultEncoderId}${hash}`;
    };

    verify = async (encoded: string, password: string) => {
        const matched = encoded.match(DelegatePasswordEncoder.HASH_PATTERN)
        if (!matched) return false

        const encoderId = matched[0]
        const encoder = this.encoders[encoderId]
        return await encoder.verify(encoded.substring(encoderId.length), password);
    };
}

export const pw = new DelegatePasswordEncoder({'argon2':ARGON2_PASSWORD_ENCODER}, 'argon2')
